package org.alerve.android.resourcebuilder;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

/**
 * 
 * Generic Director used to interact with a registered set of modules used to
 * initialise or add functionality to objects.
 * 
 * @author pabs87@gmail.com
 * 
 * @param <B>
 */
public abstract class AbstractDirector<B> {

	/**
	 * Used to avoid regenerating the Modules again once they are found.
	 */
	private Map<Class<?>, B> multitonCache = new HashMap<Class<?>, B>();

	/**
	 * {@link Map} that relates a class with it's module.
	 */
	private Map<Class<?>, Class<?>> classMap = new HashMap<Class<?>, Class<?>>();

	/**
	 * Default constructor that looks for the {@link Modules} annotation in the
	 * child class and registers them.
	 */
	protected AbstractDirector() {
		Modules annotation = getClass().getAnnotation(Modules.class);
		if (annotation != null) {

			Class<?>[] models = annotation.value();
			for (Class<?> target : models) {
				ModuleClass moduleClass = target
						.getAnnotation(ModuleClass.class);
				if (moduleClass != null) {
					registerModel(moduleClass.value(), target);
				} else {
					throw new RuntimeException("No @ModuleClass defined for "
							+ target.getName());
				}
			}
		}
	}

	/**
	 * 
	 * Annotation used in the definition of a concrete Director determine the
	 * registered Modules
	 * 
	 * @author pabs87@gmai.com
	 * 
	 */
	@Retention(RetentionPolicy.RUNTIME)
	public static @interface Modules {
		public Class<?>[] value();
	}

	/**
	 * 
	 * Annotation used by Modules to define to which Class they correspond with.
	 * 
	 * @author pabs87@gmail.com
	 * 
	 */
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.TYPE)
	public static @interface ModuleClass {
		public Class<?> value();
	}

	/**
	 * 
	 * Method used to register a module. It is used on construction of directors
	 * but it can be used at any time to add new modules or to override existing
	 * ones.
	 * 
	 * @param klass
	 * @param builder
	 */
	public <Type> void registerModel(Class<?> klass, Class<?> builder) {
		if (multitonCache.containsKey(klass)) {
			// remove the current module for the provided key if it exists.
			multitonCache.remove(klass);
		}
		classMap.put(klass, builder);
	}

	/**
	 * 
	 * Internal method where the mapping in between class and module occurs. It
	 * looks bottom-up for the parent classes in case there is an appropriate
	 * module for it.
	 * 
	 * @param klass
	 * @return
	 * @throws InstantiationException
	 * @throws IllegalAccessException
	 */
	@SuppressWarnings("unchecked")
	private <Type> B createModule(Class<Type> klass)
			throws InstantiationException, IllegalAccessException {
		Object result = null;
		if (classMap.containsKey(klass)) {
			result = classMap.get(klass).newInstance();
		}
		if (result == null) {
			// if the class is not valid, look for a super class
			for (Entry<Class<?>, Class<?>> entry : classMap.entrySet()) {
				if (entry.getKey().isAssignableFrom(klass)) {
					result = entry.getValue().newInstance();
					break;
				}
			}
		}
		return (B) result;
	}

	/**
	 * 
	 * Helper method to use {@link AbstractDirector#hasModuleFor(Class)
	 * hasModuleFor(Class)}
	 * 
	 * @param object
	 * @return
	 */
	public <Type> boolean hasModuleFor(Type object) {
		return hasModuleFor(object.getClass());
	}

	/**
	 * 
	 * Method used to determine if there is a relevant module for the given
	 * class.
	 * 
	 * @param klass
	 * @return
	 */
	public <Type> boolean hasModuleFor(Class<Type> klass) {
		try {
			return getModule(klass) != null;
		} catch (Exception e) {
			return false;
		}
	}

	public <Type> B getModule(Class<Type> klass) {
		B module = multitonCache.get(klass);
		try {
			if (module == null) {
				module = createModule(klass);
				multitonCache.put(klass, module);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		if (module == null) {
			throw new RuntimeException("Can't create a builder for "
					+ klass.getName());
		} else {
			return module;
		}
	}

}